/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.binding.injection;

import java.beans.*;
import java.util.*;
import org.jdesktop.observablecollections.*;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;
import ch.trackedbean.binding.beanSpec.*;
import ch.trackedbean.loaders.lazy.*;

/**
 * Adapter which implements the {@link StatusInformationInternal} interface and adds {@link PropertyChangeSupport} for all classes defined as
 * {@link ClassInformation#tracked}.<br>
 * Child classes (so were {@link ClassInformation#parentTracked} is true) were also adapted so that they use the feautures of the
 * {@link StatusInformationInternal} interface.
 * 
 * @author M. Hautle
 */
@SuppressWarnings("unchecked")
public class TrackedBeanAdapter {
	/** The name of the internal property change method */
	private static final String PROP_CH_METHOD = "_propChanged";

	/** The name of the Beanstatus field */
	private static final String BEAN_STATUS_FIELD = "_BeanStatus";

	/** The name of the property change support field */
	private static final String PROP_CH_SUP_FIELD = "_PropChSup";

	/** The access flags for a protected transient member */
	private static final int PROTECTED_TRANSIENT = Opcodes.ACC_PROTECTED + Opcodes.ACC_TRANSIENT;

	/** The access flags for a public member */
	private static final int PUBLIC_FINAL_ACCESS_FLAG = Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL;

	/** The access flags for a protected member */
	private static final int PROTECTED_FINAL_ACCESS_FLAG = Opcodes.ACC_PROTECTED + Opcodes.ACC_FINAL;

	/** The internal name of the {@link StatusInformationInternal} interface */
	private static final String STATUS_INTERFACE = Type.getInternalName(StatusInformationInternal.class);

	/** The internal name of the {@link PropertyChangeSupport} class */
	private static final String PROPERTY_CH_SUP = Type.getInternalName(PropertyChangeSupport.class);

	/** The internal name of the {@link PropertyChangeListener} class */
	private static final String PROPERTY_CH_LIST = Type.getInternalName(PropertyChangeListener.class);

	/** The internal name of the {@link BeanStatusImpl} class */
	private static final String BEAN_STATUS = Type.getInternalName(BeanStatusImpl.class);

	/** The internal name of the {@link BeanStatusImpl} class */
	private static final String OBSERVABLE_MAP = Type.getInternalName(ObservableMap.class);

	/** The internal name of the {@link List} class */
	private static final String LIST = Type.getInternalName(List.class);

	/** The internal name of the {@link LazyLoaderManager} class */
	private static final String LAZY_MANAGER = Type.getInternalName(LazyLoaderManager.class);

	/** The description of the {@link StatusInformationInternal#_getBeanStatus()} method */
	private static final String GET_BEAN_STATUS_M_DESC = "()L" + BEAN_STATUS + ";";

	/** The description of the {@link LoadLazy} annotation class */
	private static final String LAZY_ANNOTATION_DESC = "L" + Type.getInternalName(LoadLazy.class) + ";";

	/** The description of the {@link BeanStatusImpl} field */
	private static final String BEAN_STATUS_FIELD_DESC = "L" + BEAN_STATUS + ";";

	/** The description of the {@link BeanStatusImpl} constructor */
	private static final String BEAN_STATUS_CONST_DESC = "(L" + Type.getInternalName(Object.class) + ";)V";

	/** The description of the {@link StatusInformationInternal#_getBeanStatusMap()} method */
	private static final String GET_BEAN_STATUS_MAP_M_DESC = "()L" + OBSERVABLE_MAP + ";";

	/** The description of the propertyChangeListener methods */
	private static final String PROPERTY_CH_LIST_M_DESC = "(L" + PROPERTY_CH_LIST + ";)V";

	/** The description of the {@link PropertyChangeSupport} field */
	private static final String PROPERTY_CH_SUP_FIELD_DESC = "L" + PROPERTY_CH_SUP + ";";

	/** The OP Code groups of a "normal" setter */
	private static final int[] NORMAL_SETTER = new int[] { AbstractInsnNode.VAR_INSN, AbstractInsnNode.VAR_INSN, AbstractInsnNode.FIELD_INSN };

	/** The OP Code groups of a "normal" getter */
	private static final int[] NORMAL_GETTER = new int[] { AbstractInsnNode.VAR_INSN, AbstractInsnNode.FIELD_INSN };

	/** The class analyzer to use. */
	private final IClassAnalyzer classAnalyzer;

	/**
	 * Default constructor.
	 * 
	 * @param analyzer The analyzer to use
	 */
	public TrackedBeanAdapter(IClassAnalyzer analyzer) {
		classAnalyzer = analyzer;
	}

	/**
	 * Adapts the class represented by the given byte array.
	 * 
	 * @param b The class file as byte array
	 * @return The adapter byte array
	 */
	public byte[] adapt(byte[] b) {
		final ClassNode cn = new ClassNode();
		new ClassReader(b).accept(cn, 0);
		// transform the class, if no tranformations were done, then use the original class
		if (adapt(cn)) {
			final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
			cn.accept(cw);
			return cw.toByteArray();
		}
		return b;
	}

	/**
	 * Adapts the class if necessary
	 * 
	 * @param cn The node representing the class
	 * @return True if the class was adapted
	 */
	private boolean adapt(final ClassNode cn) {
		final ClassInformation ci = classAnalyzer.getInformation(cn.name);
		// implement StatusInformation and adapt the setters
		if (ci.tracked && !ci.parentTracked)
			adaptCompleteClass(cn, ci.serializable);
		// just adapt the setters
		else if (ci.parentTracked)
			adaptSettersOnly(cn, ci.serializable);
		// nothing to do
		else
			return false;
		return true;
	}

	/**
	 * Adapts all setter methods of the given class.<br>
	 * Used for classes where a parent class implements "TrackedBean"
	 * 
	 * @param cn The class to adapt
	 * @param serializable If the class is serializable
	 */
	private void adaptSettersOnly(final ClassNode cn, final boolean serializable) {
		final List methods = cn.methods;
		// define the a readObject method node if the class is serializable
		InsnList deserialInst = null;
		MethodNode deserialMeth = null;
		// if the class is serializable, then prepare a readObject method
		if (serializable) {
			deserialMeth = new MethodNode(Opcodes.ACC_PRIVATE, "readObject", "(Ljava/io/ObjectInputStream;)V", null, new String[] { "java/io/IOException",
					"java/lang/ClassNotFoundException" });
			deserialInst = deserialMeth.instructions;
		}
		// string builder for property name extraction
		final StringBuilder propertyName = new StringBuilder();
		for (int i = 0, cnt = methods.size(); i < cnt; i++) {
			final MethodNode m = (MethodNode) methods.get(i);
			final String methodName = m.name;
			// potential getter/setter
			if (adaptable(m.access) && methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3)))
				adaptProperty(deserialInst, propertyName, m);
		}
		// write the readObject method (if it exists and it contains at least one operation)
		if (deserialInst != null && deserialInst.size() > 0) {
			// inversed order cause each line is inserted as first line (so the last will be the first)
			deserialInst.insert(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/ObjectInputStream", "defaultReadObject", "()V"));
			deserialInst.insert(new VarInsnNode(Opcodes.ALOAD, 1));
			deserialInst.add(new InsnNode(Opcodes.RETURN));
			cn.methods.add(deserialMeth);
		}
	}

	/**
	 * Implements the {@link StatusInformationInternal} interface in the given class and adapt all setters.
	 * 
	 * @param cn The class where to implement
	 * @param serializable True if the class is serializable
	 */
	private void adaptCompleteClass(final ClassNode cn, final boolean serializable) {
		// create the needed fields
		cn.fields.add(new FieldNode(PROTECTED_TRANSIENT, PROP_CH_SUP_FIELD, PROPERTY_CH_SUP_FIELD_DESC, null, null));
		cn.fields.add(new FieldNode(PROTECTED_TRANSIENT, BEAN_STATUS_FIELD, BEAN_STATUS_FIELD_DESC, null, null));
		// implement the status interface
		cn.interfaces.add(STATUS_INTERFACE);
		// implement the _getBeanStatus method
		MethodNode m = new MethodNode(PUBLIC_FINAL_ACCESS_FLAG, "_getBeanStatus", GET_BEAN_STATUS_M_DESC, null, null);
		InsnList instr = m.instructions;
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, cn.name, BEAN_STATUS_FIELD, BEAN_STATUS_FIELD_DESC));
		instr.add(new InsnNode(Opcodes.ARETURN));
		cn.methods.add(m);
		// implement the _getBeanStatusMap method
		m = new MethodNode(PUBLIC_FINAL_ACCESS_FLAG, "_getBeanStatusMap", GET_BEAN_STATUS_MAP_M_DESC, null, null);
		instr = m.instructions;
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, cn.name, BEAN_STATUS_FIELD, BEAN_STATUS_FIELD_DESC));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, BEAN_STATUS, "getStatusMap", GET_BEAN_STATUS_MAP_M_DESC));
		instr.add(new InsnNode(Opcodes.ARETURN));
		cn.methods.add(m);
		// implement the _propChanged method
		m = new MethodNode(PROTECTED_FINAL_ACCESS_FLAG, PROP_CH_METHOD, "(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V", null, null);
		instr = m.instructions;
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, cn.name, PROP_CH_SUP_FIELD, PROPERTY_CH_SUP_FIELD_DESC));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 1));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 2));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 3));
		instr
				.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, PROPERTY_CH_SUP, "firePropertyChange",
						"(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V"));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, cn.name, BEAN_STATUS_FIELD, BEAN_STATUS_FIELD_DESC));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 1));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 2));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 3));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, BEAN_STATUS, "propertyChanged", "(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V"));
		instr.add(new InsnNode(Opcodes.RETURN));
		cn.methods.add(m);
		// implement addPropertyChangeListener
		m = new MethodNode(Opcodes.ACC_PUBLIC, "addPropertyChangeListener", PROPERTY_CH_LIST_M_DESC, null, null);
		instr = m.instructions;
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, cn.name, PROP_CH_SUP_FIELD, PROPERTY_CH_SUP_FIELD_DESC));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 1));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, PROPERTY_CH_SUP, "addPropertyChangeListener", PROPERTY_CH_LIST_M_DESC));
		instr.add(new InsnNode(Opcodes.RETURN));
		cn.methods.add(m);
		// implement removePropertyChangeListener
		m = new MethodNode(Opcodes.ACC_PUBLIC, "removePropertyChangeListener", PROPERTY_CH_LIST_M_DESC, null, null);
		instr = m.instructions;
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, cn.name, PROP_CH_SUP_FIELD, PROPERTY_CH_SUP_FIELD_DESC));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 1));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, PROPERTY_CH_SUP, "removePropertyChangeListener", PROPERTY_CH_LIST_M_DESC));
		instr.add(new InsnNode(Opcodes.RETURN));
		cn.methods.add(m);
		// define the readObject method if the class is serializable
		InsnList deserialMeth = null;
		if (serializable) {
			m = new MethodNode(Opcodes.ACC_PRIVATE, "readObject", "(Ljava/io/ObjectInputStream;)V", null, new String[] { "java/io/IOException",
					"java/lang/ClassNotFoundException" });
			deserialMeth = m.instructions;
			deserialMeth.add(new VarInsnNode(Opcodes.ALOAD, 1));
			deserialMeth.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/io/ObjectInputStream", "defaultReadObject", "()V"));
			// initialize _PropChSup
			deserialMeth.add(new VarInsnNode(Opcodes.ALOAD, 0));
			deserialMeth.add(new TypeInsnNode(Opcodes.NEW, PROPERTY_CH_SUP));
			deserialMeth.add(new InsnNode(Opcodes.DUP));
			deserialMeth.add(new VarInsnNode(Opcodes.ALOAD, 0));
			deserialMeth.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, PROPERTY_CH_SUP, "<init>", "(Ljava/lang/Object;)V"));
			deserialMeth.add(new FieldInsnNode(Opcodes.PUTFIELD, cn.name, PROP_CH_SUP_FIELD, PROPERTY_CH_SUP_FIELD_DESC));
			// initialize _BeanStatus
			deserialMeth.add(new VarInsnNode(Opcodes.ALOAD, 0));
			deserialMeth.add(new TypeInsnNode(Opcodes.NEW, BEAN_STATUS));
			deserialMeth.add(new InsnNode(Opcodes.DUP));
			deserialMeth.add(new VarInsnNode(Opcodes.ALOAD, 0));
			deserialMeth.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, BEAN_STATUS, "<init>", BEAN_STATUS_CONST_DESC));
			deserialMeth.add(new FieldInsnNode(Opcodes.PUTFIELD, cn.name, BEAN_STATUS_FIELD, BEAN_STATUS_FIELD_DESC));
			cn.methods.add(m);
		}
		// adapt the methods and the constructors
		// string builder for property name extraction
		final StringBuilder propertyName = new StringBuilder();
		final List methods = cn.methods;
		for (int i = 0, cnt = methods.size(); i < cnt; i++) {
			m = (MethodNode) methods.get(i);
			// ignore
			if (!adaptable(m.access))
				continue;
			final String methodName = m.name;
			// if it's a constructor then insert the field initializations
			if (methodName.charAt(0) == '<')
				adaptConstructor(m, cn.name);
			// potential getter/setter
			else if (methodName.length() > 3 && Character.isUpperCase(methodName.charAt(3)))
				adaptProperty(deserialMeth, propertyName, m);
		}
		// add the return statement to the readObject method (if it exists)
		if (deserialMeth != null)
			deserialMeth.add(new InsnNode(Opcodes.RETURN));
	}

	/**
	 * Checks if a method with the given access flags is adaptable
	 * 
	 * @param access The access flags
	 * @return True if the method is adaptable
	 */
	private boolean adaptable(final int access) {
		return (access & Opcodes.ACC_NATIVE) != Opcodes.ACC_NATIVE && (access & Opcodes.ACC_ABSTRACT) != Opcodes.ACC_ABSTRACT
				&& (access & Opcodes.ACC_STATIC) != Opcodes.ACC_STATIC;
	}

	/**
	 * Checks if the passed method represents a property and adapts it if necessary.
	 * 
	 * @param serRead The node list of the readObject methode or null if there is none
	 * @param property The name of the property
	 * @param m The method node representing the getter/setter
	 */
	@SuppressWarnings("cast")
	private void adaptProperty(final InsnList serRead, final StringBuilder property, final MethodNode m) {
		final String methodName = m.name;
		if (methodName.startsWith("set")) {
			property.setLength(0);
			property.append(methodName, 3, methodName.length());
			// decapitalize only if the second character is a lower case char
			if (property.length() == 1 || Character.isLowerCase(property.charAt(1)))
				property.setCharAt(0, Character.toLowerCase(property.charAt(0)));
			adaptSetter(m, property.toString(), serRead);
			return;
		}
		// skip if the class in not serializable or if it's not a getter
		if (m.visibleAnnotations == null || !methodName.startsWith("get"))
			return;
		// look for the lazy annotation
		final List<AnnotationNode> ans = (List<AnnotationNode>) m.visibleAnnotations;
		for (int i = 0, cnt = ans.size(); i < cnt; i++) {
			if (!LAZY_ANNOTATION_DESC.equals(ans.get(i).desc))
				continue;
			property.setLength(0);
			property.append(methodName, 3, methodName.length());
			// decapitalize only if the second character is a lower case char
			if (property.length() == 1 || Character.isLowerCase(property.charAt(1)))
				property.setCharAt(0, Character.toLowerCase(property.charAt(0)));
			adaptGetter(m, property.toString(), serRead);
			return;
		}
	}

	/**
	 * Adapts the given getter for lazy loading
	 * 
	 * @param m The method node representing the getter
	 * @param property The name of the property
	 * @param serRead The node list of the readObject methode or null if there is none
	 */
	@SuppressWarnings("null")
	private void adaptGetter(final MethodNode m, final String property, final InsnList serRead) {
		final Type[] args = Type.getArgumentTypes(m.desc);
		final Type ret = Type.getReturnType(m.desc);
		// a geter has no parameter but a return type
		if (args.length != 0 || Type.VOID == ret.getSort())
			return;
		AbstractInsnNode inst = m.instructions.getFirst();
		boolean normalGetter = inst != null;
		// check the opcodes if they correspond to a normal setter...
		for (int i = 0; normalGetter && inst != null && i < NORMAL_GETTER.length;) {
			final int type = inst.getType();
			if (type != AbstractInsnNode.LABEL && type != AbstractInsnNode.LINE)
				normalGetter = (type == NORMAL_GETTER[i++]);
			inst = inst.getNext();
		}
		// get the setting instruction node (this which sets the value)
		FieldInsnNode getNode = null;
		if (normalGetter && inst != null)
			getNode = ((FieldInsnNode) inst.getPrevious());
		// if it seem to be a normal getter then the instructions following now must be of type label/line or a return
		// opcode
		while (normalGetter && inst != null) {
			final int type = inst.getType();
			normalGetter = type == AbstractInsnNode.LABEL || type == AbstractInsnNode.LINE || inst.getOpcode() == Opcodes.ARETURN;
			inst = inst.getNext();
		}
		// ignore non standard getters
		if (!normalGetter)
			return;
		switch (ret.getSort()) {
			case Type.OBJECT:
				// TODO wrap lists also in lazy loading.....
			case Type.ARRAY:
				adaptObjectGetter(property, m, ret, getNode.name, getNode.desc, getNode.owner);
				break;
			default:
				throw new RuntimeException("Unknown Getter type: " + ret.getClassName());
		}
		if (serRead != null)
			deserializeLazyAdaption(serRead, property, getNode.name, getNode.desc, getNode.owner);
	}

	/**
	 * Adapts the readObject methode so that the status of a {@link LoadLazy} annotated property will initialized.
	 * 
	 * @param serRead The node list of the readObject methode
	 * @param propertyName The name of the property represented by this getter
	 * @param fieldName The name of the field to get (so where the setter stores the value)
	 * @param fieldType The type of the field where the value is stored
	 * @param owner The owning class
	 */
	private void deserializeLazyAdaption(final InsnList serRead, final String propertyName, final String fieldName, final String fieldType, final String owner) {
		final LabelNode blockEnd = new LabelNode();
		serRead.add(new VarInsnNode(Opcodes.ALOAD, 0));
		serRead.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		serRead.add(new JumpInsnNode(Opcodes.IFNONNULL, blockEnd));
		serRead.add(new VarInsnNode(Opcodes.ALOAD, 0));
		serRead.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, owner, "_getBeanStatus", GET_BEAN_STATUS_M_DESC));
		serRead.add(new LdcInsnNode(propertyName));
		serRead.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, BEAN_STATUS, "setLazy", "(Ljava/lang/String;)V"));
		serRead.add(blockEnd);
	}

	/**
	 * Adapts a getter for objects or arrays
	 * 
	 * @param propertyName The name of the property represented by this getter
	 * @param m The getter method
	 * @param returnType The returntype of the getter
	 * @param fieldName The name of the field to get (so where the setter stores the value)
	 * @param fieldType The type of the field where the value is stored
	 * @param owner The owning class
	 */
	private void adaptObjectGetter(final String propertyName, final MethodNode m, final Type returnType, final String fieldName, final String fieldType,
			final String owner) {
		final ListIterator instr = m.instructions.iterator();
		// the label marking the end of the load block
		final LabelNode blockEnd = new LabelNode();
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		instr.add(new JumpInsnNode(Opcodes.IFNONNULL, blockEnd));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, owner, "_getBeanStatus", GET_BEAN_STATUS_M_DESC));
		instr.add(new LdcInsnNode(propertyName));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, BEAN_STATUS, "loadField", "(Ljava/lang/String;)Z"));
		instr.add(new JumpInsnNode(Opcodes.IFEQ, blockEnd));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new LdcInsnNode(Type.getObjectType(owner)));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new LdcInsnNode(propertyName));
		instr.add(new MethodInsnNode(Opcodes.INVOKESTATIC, LAZY_MANAGER, "loadProperty",
				"(Ljava/lang/Class;Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;"));
		instr.add(new TypeInsnNode(Opcodes.CHECKCAST, returnType.getInternalName()));
		instr.add(new FieldInsnNode(Opcodes.PUTFIELD, owner, fieldName, fieldType));
		instr.add(blockEnd);
	}

	/**
	 * Adapts the given setter so that it notifies listeners when it's called
	 * 
	 * @param m The method node representing the setter
	 * @param property The name of the property
	 * @param serRead The node list of the readObject methode or null if there is none
	 */
	@SuppressWarnings("null")
	private void adaptSetter(final MethodNode m, final String property, final InsnList serRead) {
		final Type[] args = Type.getArgumentTypes(m.desc);
		// a setter has only one parameter...
		if (args.length != 1)
			return;
		AbstractInsnNode inst = m.instructions.getFirst();
		boolean normalSetter = inst != null;
		// check the opcodes if they correspond to a normal setter...
		for (int i = 0; normalSetter && inst != null && i < NORMAL_SETTER.length;) {
			final int type = inst.getType();
			if (type != AbstractInsnNode.LABEL && type != AbstractInsnNode.LINE)
				normalSetter = (type == NORMAL_SETTER[i++]);
			inst = inst.getNext();
		}
		// get the setting instruction node (this which sets the value)
		FieldInsnNode setNode = null;
		if (normalSetter && inst != null)
			setNode = ((FieldInsnNode) inst.getPrevious());
		// if it seem to be a normal setter then the instructions following now must be of type label/line or a return
		// opcode
		while (normalSetter && inst != null) {
			final int type = inst.getType();
			normalSetter = type == AbstractInsnNode.LABEL || type == AbstractInsnNode.LINE || inst.getOpcode() == Opcodes.RETURN;
			inst = inst.getNext();
		}
		// ignore non standard setters
		if (!normalSetter)
			return;
		switch (args[0].getSort()) {
			case Type.OBJECT:
				// figure out the setter type
				final String name = args[0].getInternalName();
				if (name.equals(LIST)) {
					adaptListSetter(property, m, setNode.name, setNode.desc, setNode.owner, serRead);
					break;
				} else if (implementsClassStructureFlag(name)) {
					adaptBeanSetter(property, m, setNode.name, setNode.desc, setNode.owner, serRead);
					break;
				}
			case Type.ARRAY:
				adaptObjectSetter(property, m, setNode.name, setNode.desc, setNode.owner);
				break;
			case Type.INT:
				adaptPrimitiveSetter(property, m, "java/lang/Integer", "(I)V", setNode.name, setNode.desc, setNode.owner);
				break;
			case Type.LONG:
				adaptBigPrimitiveSetter(property, m, "java/lang/Long", "(J)V", setNode.name, setNode.desc, setNode.owner);
				break;
			case Type.BOOLEAN:
				adaptPrimitiveSetter(property, m, "java/lang/Boolean", "(Z)V", setNode.name, setNode.desc, setNode.owner);
				break;
			case Type.DOUBLE:
				adaptBigPrimitiveSetter(property, m, "java/lang/Double", "(D)V", setNode.name, setNode.desc, setNode.owner);
				break;
			case Type.FLOAT:
				adaptPrimitiveSetter(property, m, "java/lang/Float", "(F)V", setNode.name, setNode.desc, setNode.owner);
				break;
			case Type.SHORT:
				adaptPrimitiveSetter(property, m, "java/lang/Short", "(S)V", setNode.name, setNode.desc, setNode.owner);
				break;
			case Type.CHAR:
				adaptPrimitiveSetter(property, m, "java/lang/Character", "(C)V", setNode.name, setNode.desc, setNode.owner);
				break;
			case Type.BYTE:
				adaptPrimitiveSetter(property, m, "java/lang/Byte", "(B)V", setNode.name, setNode.desc, setNode.owner);
				break;
			default:
				throw new RuntimeException("Unknown Setter type: " + args[0].getClassName());
		}
	}

	/**
	 * Injects the code for the initializing of the fields _PropChSup and _BeanStatus
	 * 
	 * @param m The constructor to patch
	 * @param className The internal name of the class where this constructor is defined
	 */
	private void adaptConstructor(final MethodNode m, final String className) {
		final ListIterator instr = m.instructions.iterator();
		AbstractInsnNode inst = (AbstractInsnNode) instr.next();
		// search the super call
		while (inst != null && inst.getType() != AbstractInsnNode.METHOD_INSN)
			inst = (AbstractInsnNode) instr.next();
		// and attach the initializing sequence
		// initialize _PropChSup
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new TypeInsnNode(Opcodes.NEW, PROPERTY_CH_SUP));
		instr.add(new InsnNode(Opcodes.DUP));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, PROPERTY_CH_SUP, "<init>", "(Ljava/lang/Object;)V"));
		instr.add(new FieldInsnNode(Opcodes.PUTFIELD, className, PROP_CH_SUP_FIELD, PROPERTY_CH_SUP_FIELD_DESC));
		// initialize _BeanStatus
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new TypeInsnNode(Opcodes.NEW, BEAN_STATUS));
		instr.add(new InsnNode(Opcodes.DUP));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, BEAN_STATUS, "<init>", BEAN_STATUS_CONST_DESC));
		instr.add(new FieldInsnNode(Opcodes.PUTFIELD, className, BEAN_STATUS_FIELD, BEAN_STATUS_FIELD_DESC));
	}

	/**
	 * Adapts setters for primitive values (not for Long/Double, for those use
	 * {@link #adaptBigPrimitiveSetter(String, MethodNode, String, String, String, String, String)})
	 * 
	 * @param propertyName The name of the property represented by this setter
	 * @param m The setter method
	 * @param wrapperType The wrapper type for this simple type
	 * @param wraperDesc The description for the wraper type constructor
	 * @param fieldName The name of the field to set (so where the setter stores the value)
	 * @param fieldType The type of the field where the value is stored
	 * @param owner The owning class
	 */
	private void adaptPrimitiveSetter(final String propertyName, final MethodNode m, final String wrapperType, final String wraperDesc, final String fieldName,
			final String fieldType, final String owner) {
		final ListIterator instr = m.instructions.iterator();
		// code to store the old value
		instr.add(new TypeInsnNode(Opcodes.NEW, wrapperType));
		instr.add(new InsnNode(Opcodes.DUP));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		instr.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, wrapperType, "<init>", wraperDesc));
		instr.add(new VarInsnNode(Opcodes.ASTORE, 2));
		AbstractInsnNode inst = (AbstractInsnNode) instr.next();
		// search return statement
		while (inst != null && inst.getOpcode() != Opcodes.RETURN)
			inst = (AbstractInsnNode) instr.next();
		// insert code to fire the property change event before the return statement
		instr.previous();
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new LdcInsnNode(propertyName));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 2));
		instr.add(new TypeInsnNode(Opcodes.NEW, wrapperType));
		instr.add(new InsnNode(Opcodes.DUP));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		instr.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, wrapperType, "<init>", wraperDesc));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, owner, PROP_CH_METHOD, "(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V"));
	}

	/**
	 * Adapts setters for long and double values (for other primitive values use
	 * {@link #adaptPrimitiveSetter(String, MethodNode, String, String, String, String, String)})
	 * 
	 * @param propertyName The name of the property represented by this setter
	 * @param m The setter method
	 * @param wrapperType The wrapper type for this simple type
	 * @param wraperDesc The description for the wraper type constructor
	 * @param fieldName The name of the field to set (so where the setter stores the value)
	 * @param fieldType The type of the field where the value is stored
	 * @param owner The owning class
	 */
	private void adaptBigPrimitiveSetter(final String propertyName, final MethodNode m, final String wrapperType, final String wraperDesc,
			final String fieldName, final String fieldType, final String owner) {
		final ListIterator instr = m.instructions.iterator();
		// code to store the old value
		instr.add(new TypeInsnNode(Opcodes.NEW, wrapperType));
		instr.add(new InsnNode(Opcodes.DUP));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		instr.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, wrapperType, "<init>", wraperDesc));
		instr.add(new VarInsnNode(Opcodes.ASTORE, 3));
		AbstractInsnNode inst = (AbstractInsnNode) instr.next();
		// search return statement
		while (inst != null && inst.getOpcode() != Opcodes.RETURN)
			inst = (AbstractInsnNode) instr.next();
		// insert code to fire the property change event before the return statement
		instr.previous();
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new LdcInsnNode(propertyName));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 3));
		instr.add(new TypeInsnNode(Opcodes.NEW, wrapperType));
		instr.add(new InsnNode(Opcodes.DUP));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		instr.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, wrapperType, "<init>", wraperDesc));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, owner, PROP_CH_METHOD, "(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V"));
	}

	/**
	 * Adapts a setter for objects or arrays
	 * 
	 * @param propertyName The name of the property represented by this setter
	 * @param m The setter method
	 * @param fieldName The name of the field to set (so where the setter stores the value)
	 * @param fieldType The type of the field where the value is stored
	 * @param owner The owning class
	 */
	private void adaptObjectSetter(final String propertyName, final MethodNode m, final String fieldName, final String fieldType, final String owner) {
		final ListIterator instr = m.instructions.iterator();
		// code to store the old value
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		instr.add(new VarInsnNode(Opcodes.ASTORE, 2));
		AbstractInsnNode inst = (AbstractInsnNode) instr.next();
		// search return statement
		while (inst != null && inst.getOpcode() != Opcodes.RETURN)
			inst = (AbstractInsnNode) instr.next();
		// insert code to fire the property change event before the return statement
		instr.previous();
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new LdcInsnNode(propertyName));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 2));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, owner, PROP_CH_METHOD, "(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V"));
	}

	/**
	 * Adapts a setter for {@link List}s
	 * 
	 * @param propertyName The name of the property represented by this setter
	 * @param m The setter method
	 * @param fieldName The name of the field to set (so where the setter stores the value)
	 * @param fieldType The type of the field where the value is stored
	 * @param owner The owning class
	 * @param serRead The node list of the readObject methode or null if there is none
	 */
	private void adaptListSetter(final String propertyName, final MethodNode m, final String fieldName, final String fieldType, final String owner,
			final InsnList serRead) {
		final ListIterator instr = m.instructions.iterator();
		// code to store the old value
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		instr.add(new VarInsnNode(Opcodes.ASTORE, 2));
		// call BeanStatus.registerChildList() to adapt the given list and store it instead of the given parameter
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, BEAN_STATUS_FIELD, BEAN_STATUS_FIELD_DESC));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 2));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 1));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, BEAN_STATUS, "registerChildList", "(Ljava/util/List;Ljava/util/List;)Ljava/util/List;"));
		instr.add(new VarInsnNode(Opcodes.ASTORE, 1));

		// search return statement
		AbstractInsnNode inst = (AbstractInsnNode) instr.next();
		while (inst != null && inst.getOpcode() != Opcodes.RETURN)
			inst = (AbstractInsnNode) instr.next();
		// insert code to fire the property change event before the return statement
		instr.previous();
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new LdcInsnNode(propertyName));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 2));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, owner, PROP_CH_METHOD, "(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V"));
		// if the class is serializable then wrap the list of this setter after the deserializing
		if (serRead != null) {
			serRead.add(new VarInsnNode(Opcodes.ALOAD, 0));
			serRead.add(new VarInsnNode(Opcodes.ALOAD, 0));
			serRead.add(new FieldInsnNode(Opcodes.GETFIELD, owner, BEAN_STATUS_FIELD, BEAN_STATUS_FIELD_DESC));
			serRead.add(new InsnNode(Opcodes.ACONST_NULL));
			serRead.add(new VarInsnNode(Opcodes.ALOAD, 0));
			serRead.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
			serRead.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, BEAN_STATUS, "registerChildList", "(Ljava/util/List;Ljava/util/List;)Ljava/util/List;"));
			serRead.add(new FieldInsnNode(Opcodes.PUTFIELD, owner, fieldName, fieldType));
		}
	}

	/**
	 * Adapts a setter for "TrackedBeans"
	 * 
	 * @param propertyName The name of the property represented by this setter
	 * @param m The setter method
	 * @param fieldName The name of the field to set (so where the setter stores the value)
	 * @param fieldType The type of the field where the value is stored
	 * @param owner The owning class
	 * @param serRead The node list of the readObject methode or null if there is none
	 */
	private void adaptBeanSetter(final String propertyName, final MethodNode m, final String fieldName, final String fieldType, final String owner,
			final InsnList serRead) {
		final ListIterator instr = m.instructions.iterator();
		// code to store the old value
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		instr.add(new VarInsnNode(Opcodes.ASTORE, 2));

		// call BeanStatus.registerChild() to register the given bean
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, BEAN_STATUS_FIELD, BEAN_STATUS_FIELD_DESC));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 2));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 1));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, BEAN_STATUS, "registerChild", "(Ljava/lang/Object;Ljava/lang/Object;)V"));

		// search return statement
		AbstractInsnNode inst = (AbstractInsnNode) instr.next();
		while (inst != null && inst.getOpcode() != Opcodes.RETURN)
			inst = (AbstractInsnNode) instr.next();
		// insert code to fire the property change event before the return statement
		instr.previous();
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new LdcInsnNode(propertyName));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 2));
		instr.add(new VarInsnNode(Opcodes.ALOAD, 0));
		instr.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
		instr.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, owner, PROP_CH_METHOD, "(Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;)V"));
		// if the class is serializable then register the bean of this setter after the deserializing
		if (serRead != null) {
			serRead.add(new VarInsnNode(Opcodes.ALOAD, 0));
			serRead.add(new FieldInsnNode(Opcodes.GETFIELD, owner, BEAN_STATUS_FIELD, BEAN_STATUS_FIELD_DESC));
			serRead.add(new InsnNode(Opcodes.ACONST_NULL));
			serRead.add(new VarInsnNode(Opcodes.ALOAD, 0));
			serRead.add(new FieldInsnNode(Opcodes.GETFIELD, owner, fieldName, fieldType));
			serRead.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, BEAN_STATUS, "registerChild", "(Ljava/lang/Object;Ljava/lang/Object;)V"));
		}
	}

	/**
	 * Analyze the class structure of the given class
	 * 
	 * @param name The class your interested in
	 * @return True if the class or one of it's parents has the marker interface implemented
	 */
	private boolean implementsClassStructureFlag(final String name) {
		final ClassInformation known = classAnalyzer.getInformation(name);
		return known.tracked || known.parentTracked;
	}
}
